package com.gowiny.ddns.service;

import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import com.gowiny.ddns.core.handler.DdnsHandler;
import com.gowiny.ddns.core.key.AppKey;
import com.gowiny.ddns.core.utils.AppUtils;
import com.gowiny.ddns.core.utils.HttpUtils;
import com.gowiny.ddns.core.utils.JsonUtils;
import com.gowiny.ddns.core.vo.DomainRecord;
import com.gowiny.ddns.core.vo.Manifest;
import com.gowiny.ddns.core.vo.NetworkType;

@Component
@Service
public class DdnsServiceImpl implements DdnsService {

	private static final Logger logger = LoggerFactory.getLogger(DdnsServiceImpl.class);
	private static Map<String,NetworkType> defaultNetworkTypes;
	
	@Autowired
	private List<DdnsHandler> handlers;
	
	@Value("classpath:manifest.json")
	private Resource defaultConfigFile;

	private String configFile;
	private Manifest manifest;
	private boolean running;
	private Map<String,String> argMap;
	private File storeFile;
	
	public static Map<String, NetworkType> getDefaultNetworkTypes() {
		if(null == defaultNetworkTypes) {
			defaultNetworkTypes = new HashMap<String, NetworkType>();
			NetworkType item = new NetworkType();
			item.setUrl("https://api.ipify.org?format=json");
			item.setProp("ip");
			defaultNetworkTypes.put(AppKey.TYPE_IPV4, item);
			
			item = new NetworkType();
			item.setUrl("https://api64.ipify.org?format=json");
			item.setProp("ip");
			item.setRule(".*:.*");
			defaultNetworkTypes.put(AppKey.TYPE_IPV6, item);
		}
		return defaultNetworkTypes;
	}
	public Manifest loadManifest() throws Exception {
        String json = null;
        if(StringUtils.isEmpty(configFile)) {
        	configFile = "manifest.json";
        }
        File confFile = new File(configFile);
        if(confFile.exists() && 
        		confFile.canRead()){
            if(logger.isInfoEnabled()){
                logger.info("use config-file:{}",confFile.getAbsolutePath());
            }
            json = FileUtils.readFileToString(confFile, StandardCharsets.UTF_8.name());
        }else if(null != defaultConfigFile &&
        		defaultConfigFile.exists()){
            if(logger.isInfoEnabled()){
                logger.info("load config from classpath");
            }
            InputStream input = defaultConfigFile.getInputStream();
            json = IOUtils.toString(input, StandardCharsets.UTF_8.name());
            input.close();
        }
        if(StringUtils.isEmpty(json)) {
        	throw new Exception("conf is required");
        }
        Manifest manifest = JsonUtils.fromJson(json,Manifest.class);
        if(StringUtils.isEmpty(manifest.getId())){
            manifest.setId("0");
        }

        Map<String,NetworkType> types = manifest.getTypes();
        
        if(null == types) {
        	types = new HashMap<String, NetworkType>();
        	manifest.setTypes(types);
        }
        
        if(types.isEmpty()) {
        	types.put(AppKey.TYPE_IPV4, new NetworkType(true));
        	types.put(AppKey.TYPE_IPV6, new NetworkType(true));
        }
        
        if(null == manifest.getPeriod()) {
        	manifest.setPeriod(0);
        }
        
        if(StringUtils.isEmpty(manifest.getMode())) {
        	manifest.setMode(AppKey.MODE_REMOTE_URL);
        }

        return manifest;
    }
	private void initStoreFile(){
        //String userHome = System.getProperty("user.home");
        //storeFile = new File(userHome, String.format(".gowiny-ddns/ip-%s.json",manifest.getId()));
		storeFile = new File("last.json");
		if(logger.isInfoEnabled()) {
        	logger.info("storeFile={}",storeFile.getAbsolutePath());
        }
		/*
        if(!storeFile.getParentFile().exists()){
            storeFile.getParentFile().mkdirs();
        }
        */
    }
	private void init() throws  Exception{
        if(null == manifest){
        	getDefaultNetworkTypes();
        	System.setProperty("java.net.preferIPv6Addresses", "true");
            if(logger.isInfoEnabled()){
                logger.info("init begin");
            }
            manifest = loadManifest();
            initStoreFile();
            if(logger.isInfoEnabled()){
                logger.info("init success");
            }
        }
    }
	
	private Map<String,String> parseArgs(String[] args){
		Map<String,String> map = new HashMap<String, String>();
		for(String item:args) {
			if(item.startsWith("--")) {
				int index = item.indexOf("=");
				String name,value;
				if(index > -1) {
					name = item.substring(2,index);
					value = item.substring(index+1);
				}else {
					name = item.substring(2);
					value = null;
				}
				
				map.put(name, value);
			}
		}
		return map;
	}
	@Override
	public void startup(String[] args) throws Exception {
		argMap = parseArgs(args);
		String listIpMac = argMap.get(AppKey.ARG_NAME_LIST_IP);
		if(StringUtils.isNotEmpty(listIpMac)) {
			if(logger.isInfoEnabled()) {
				logger.info("list ip by mac:{}",listIpMac);
			}
			String[] ips = AppUtils.getIpsByMac(listIpMac);
			for(int i=0;i<ips.length;i++) {
				System.out.println(i+":"+ips[i]);
			}
			return;
		}
		configFile = argMap.get(AppKey.ARG_NAME_CONFIG_FILE);
		running = true;
		try{
            do{
                handle();
                if(running && manifest.getPeriod() > 0){
                    Thread.sleep(manifest.getPeriod() * 1000);
                }
            }while (running && manifest.getPeriod() > 0);
        }catch (Exception e){
            if(logger.isErrorEnabled()){
                logger.error("更新记录出错,原因:"+e.getMessage(),e);
            }
        }
	}
	
	@SuppressWarnings("unchecked")
	private Map<String,String> loadOldIpInfo() throws Exception{
		Map<String,String> result = null;
        if(storeFile.exists() &&
                storeFile.canRead()){
            String json = FileUtils.readFileToString(storeFile, StandardCharsets.UTF_8.name());
            if(StringUtils.isNotEmpty(json)){
                result = JsonUtils.fromJson(json,Map.class);
            }
        }
        if(null == result){
            result = new HashMap<>();
        }
        return result;
    }
	@SuppressWarnings("unchecked")
	private String fetchIpFromUrl(NetworkType conf,NetworkType defConf) throws Exception{
        String ip = null;
        if(!Boolean.FALSE.equals(conf.getEnable()) ){
            String url = conf.getUrl();
            String prop = conf.getProp();
            String regex = conf.getRegex();
            String rule = conf.getRule();
            if(StringUtils.isEmpty(url)){
                url = defConf.getUrl();
                prop = defConf.getProp();
                regex = defConf.getRegex();
                rule = defConf.getRule();
            }
            String json = HttpUtils.getString(url);
            if(StringUtils.isNotEmpty(json)){
                if(StringUtils.isEmpty(regex)){
					Map<String,Object> data = JsonUtils.fromJson(json,Map.class);
                    if(StringUtils.isEmpty(prop)) {
                    	prop = "ip";
                    }
					ip = BeanUtils.getProperty(data,prop);
                }else{
                    Pattern pattern = Pattern.compile(regex);
                    Matcher matcher = pattern.matcher(json);
                    if(matcher.find()){
                        ip = matcher.group(1);
                    }
                }
            }
            
            if(StringUtils.isNotEmpty(ip) &&
					StringUtils.isNotEmpty(rule) &&
					!ip.matches(rule)
					) {
				ip = null;
			}
        }
        return ip;
    }
	
	
	private Map<String,String> fetchNewIpInfo() throws Exception{
		Map<String,String> info = new HashMap<String, String>();
		Map<String,NetworkType> types = manifest.getTypes();
		String defaultMode = manifest.getMode();
		String defaultMac = manifest.getMac();
		for(Entry<String,NetworkType> entry:types.entrySet() ) {
			String name = entry.getKey();
			NetworkType networkType = entry.getValue();
			String mode = networkType.getMode();
			if(StringUtils.isEmpty(mode)) {
				mode = defaultMode;
			}
			String ip;
			if(AppKey.MODE_LOCAL_MAC.equals(mode)) {
				String mac = networkType.getMac();
				if(StringUtils.isEmpty(mac)) {
					mac = defaultMac;
				}
				ip = AppUtils.getIpByMac(mac, networkType.getIndex());
				String rule = networkType.getRule();
				if(StringUtils.isNotEmpty(ip) &&
						StringUtils.isNotEmpty(rule) &&
						!ip.matches(rule)
						) {
					ip = null;
				}
				
			}else {
				ip = fetchIpFromUrl(networkType,defaultNetworkTypes.get(name));
			}
			if(StringUtils.isNotEmpty(ip)) {
				info.put(name, ip);
			}
    	}
        return info;
    }
	
	private void updateDomainRecord(DomainRecord domainRecord,Map<String,String> ipInfo){
		for(DdnsHandler handler : handlers) {
			Map<String,Object> provider = domainRecord.getProvider();
			if(null == provider) {
				provider = manifest.getProvider();
				domainRecord.setProvider(provider);
			}
			String type = MapUtils.getString(provider, AppKey.NAME_PROVIDER_TYPE);
			if(StringUtils.isEmpty(type)) {
				type = AppKey.DEFAULT_PROVIDER_TYPE;
			}
			if(handler.support(type)) {
				handler.handle(manifest, domainRecord, ipInfo);
				break;
			}
		}
	}
	
	private void handle() throws Exception {
		init();
		Map<String,String> oldIps = loadOldIpInfo();
		Map<String,String> newIps = fetchNewIpInfo();
		Map<String,String> changedIps = new HashMap<String, String>();
		for(Entry<String,String> entry:newIps.entrySet()) {
			String key = entry.getKey();
			String value = entry.getValue();
			if(!oldIps.containsKey(key) ||
					!value.equals(oldIps.get(key))) {
				changedIps.put(key, value);
			}
		}
		if(changedIps.isEmpty()) {
			if(logger.isInfoEnabled()){
	            logger.info("no change");
	        }
			return;
		}
		
		List<DomainRecord> records = manifest.getRecords();
        for(DomainRecord item:records){
            updateDomainRecord(item,changedIps);
        }
        if(logger.isInfoEnabled()){
            logger.info("update success");
        }
        FileUtils.writeStringToFile(storeFile, JsonUtils.toJson(newIps),StandardCharsets.UTF_8.name());
        if(logger.isInfoEnabled()){
            logger.info("save ip");
        }
	}
	

	@Override
	public void shutdown() {
		running = false;
		if(manifest != null) {
			manifest = null;
		}
	}
	
	
}
